/*
 * Copyright 2011 The OpenIEC61850 Authors
 *
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
 * in compliance with the License. You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software distributed under the License
 * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
 * or implied. See the License for the specific language governing permissions and limitations under
 * the License.
 */
package com.beanit.openiec61850;

import com.beanit.jasn1.ber.types.BerNull;
import com.beanit.openiec61850.internal.mms.asn1.Data;
import com.beanit.openiec61850.internal.mms.asn1.TypeDescription;
import com.beanit.openiec61850.internal.mms.asn1.UtcTime;
import java.util.Calendar;
import java.util.Date;

public final class BdaTimestamp extends BasicDataAttribute {

  private volatile byte[] value;

  public BdaTimestamp(
      ObjectReference objectReference, Fc fc, String sAddr, boolean dchg, boolean dupd) {
    super(objectReference, fc, sAddr, dchg, dupd);
    basicType = BdaType.TIMESTAMP;
    setDefault();
  }

  /**
   * The SecondSinceEpoch shall be the interval in seconds continuously counted from the epoch
   * 1970-01-01 00:00:00 UTC
   */

  /**
   * Returns the value as the number of seconds since epoch 1970-01-01 00:00:00 UTC
   *
   * @return the number of seconds since epoch 1970-01-01 00:00:00 UTC
   */
  private long getSecondsSinceEpoch() {
    return ((0xffL & value[0]) << 24
        | (0xffL & value[1]) << 16
        | (0xffL & value[2]) << 8
        | (0xffL & value[3]));
  }

  /**
   * The attribute FractionOfSecond shall be the fraction of the current second when the value of
   * the TimeStamp has been determined. The fraction of second shall be calculated as <code>
   * (SUM from I = 0 to 23 of bi*2**–(I+1) s).</code> NOTE 1 The resolution is the smallest unit by
   * which the time stamp is updated. The 24 bits of the integer provides 1 out of 16777216 counts
   * as the smallest unit; calculated by 1/2**24 which equals approximately 60 ns.
   *
   * <p>NOTE 2 The resolution of a time stamp may be 1/2**1 (= 0,5 s) if only the first bit is used;
   * or may be 1/2**2 (= 0,25 s) if the first two bits are used; or may be approximately 60 ns if
   * all 24 bits are used. The resolution provided by an IED is outside the scope of this standard.
   *
   * @return the fraction of seconds
   */
  private int getFractionOfSecond() {
    return ((0xff & value[4]) << 16 | (0xff & value[5]) << 8 | (0xff & value[6]));
  }

  public void setDate(
      Date date,
      boolean leapSecondsKnown,
      boolean clockFailure,
      boolean clockNotSynchronized,
      int timeAccuracy) {
    if (value == null) {
      value = new byte[8];
    }

    int secondsSinceEpoch = (int) (date.getTime() / 1000L);
    int fractionOfSecond = (int) ((date.getTime() % 1000L) / 1000.0 * (1 << 24));

    int timeQuality = timeAccuracy & 0x1f;
    if (leapSecondsKnown) {
      timeQuality = timeQuality | 0x80;
    }
    if (clockFailure) {
      timeQuality = timeQuality | 0x40;
    }
    if (clockNotSynchronized) {
      timeQuality = timeQuality | 0x20;
    }

    value =
        new byte[] {
          (byte) ((secondsSinceEpoch >> 24) & 0xff),
          (byte) ((secondsSinceEpoch >> 16) & 0xff),
          (byte) ((secondsSinceEpoch >> 8) & 0xff),
          (byte) (secondsSinceEpoch & 0xff),
          (byte) ((fractionOfSecond >> 16) & 0xff),
          (byte) ((fractionOfSecond >> 8) & 0xff),
          (byte) (fractionOfSecond & 0xff),
          (byte) timeQuality
        };
  }

  @Override
  public void setValueFrom(BasicDataAttribute bda) {
    byte[] srcValue = ((BdaTimestamp) bda).getValue();
    if (value.length != srcValue.length) {
      value = new byte[srcValue.length];
    }
    System.arraycopy(srcValue, 0, value, 0, srcValue.length);
  }

  public Date getDate() {
    if (value == null || value.length == 0) {
      return null;
    }
    long time =
        getSecondsSinceEpoch() * 1000L
            + (long) (((float) getFractionOfSecond()) / (1 << 24) * 1000 + 0.5);
    return new Date(time);
  }

  public void setDate(Date date) {
    if (value == null) {
      value = new byte[8];
    }

    int secondsSinceEpoch = (int) (date.getTime() / 1000L);
    int fractionOfSecond = (int) ((date.getTime() % 1000L) / 1000.0 * (1 << 24));

    // 0x8a = time accuracy of 10 and LeapSecondsKnown = true, ClockFailure
    // = false, ClockNotSynchronized = false
    value =
        new byte[] {
          (byte) ((secondsSinceEpoch >> 24) & 0xff),
          (byte) ((secondsSinceEpoch >> 16) & 0xff),
          (byte) ((secondsSinceEpoch >> 8) & 0xff),
          (byte) (secondsSinceEpoch & 0xff),
          (byte) ((fractionOfSecond >> 16) & 0xff),
          (byte) ((fractionOfSecond >> 8) & 0xff),
          (byte) (fractionOfSecond & 0xff),
          (byte) 0x8a
        };
  }

  public byte[] getValue() {
    return value;
  }

  public void setValue(byte[] value) {
    if (value == null) {
      this.value = new byte[8];
    }
    this.value = value;
  }

  /**
   * The value TRUE of the attribute LeapSecondsKnown shall indicate that the value for
   * SecondSinceEpoch takes into account all leap seconds occurred. If it is FALSE then the value
   * does not take into account the leap seconds that occurred before the initialization of the time
   * source of the device.
   *
   * <p>Java {@link Date} and {@link Calendar} objects do handle leap seconds, so this is usually
   * true.
   *
   * @return TRUE of the attribute LeapSecondsKnown shall indicate that the value for
   *     SecondSinceEpoch takes into account all leap seconds occurred
   */
  public boolean getLeapSecondsKnown() {
    return ((value[7] & 0x80) != 0);
  }

  /**
   * The attribute clockFailure shall indicate that the time source of the sending device is
   * unreliable. The value of the TimeStamp shall be ignored.
   *
   * @return true if the time source of the sending device is unreliable
   */
  public boolean getClockFailure() {
    return ((value[7] & 0x40) != 0);
  }

  /**
   * The attribute clockNotSynchronized shall indicate that the time source of the sending device is
   * not synchronized with the external UTC time.
   *
   * @return true if the time source of the sending device is not synchronized
   */
  public boolean getClockNotSynchronized() {
    return ((value[7] & 0x20) != 0);
  }

  /**
   * The attribute TimeAccuracy shall represent the time accuracy class of the time source of the
   * sending device relative to the external UTC time. The timeAccuracy classes shall represent the
   * number of significant bits in the FractionOfSecond
   *
   * <p>If the time is set via Java {@link Date} objects, the accuracy is 1 ms, that is a
   * timeAccuracy value of 10.
   *
   * @return the time accuracy
   */
  public int getTimeAccuracy() {
    return ((value[7] & 0x1f));
  }

  /** Sets Timestamp the empty byte array (indicating an invalid Timestamp) */
  @Override
  public void setDefault() {
    value = new byte[8];
  }

  /** Sets Timestamp to current time */
  public void setCurrentTime() {
    setDate(new Date());
  }

  @Override
  public BdaTimestamp copy() {
    BdaTimestamp copy = new BdaTimestamp(objectReference, fc, sAddr, dchg, dupd);
    byte[] valueCopy = new byte[value.length];
    System.arraycopy(value, 0, valueCopy, 0, value.length);
    copy.setValue(valueCopy);
    if (mirror == null) {
      copy.mirror = this;
    } else {
      copy.mirror = mirror;
    }
    return copy;
  }

  @Override
  Data getMmsDataObj() {
    Data data = new Data();
    data.setUtcTime(new UtcTime(value));
    return data;
  }

  @Override
  void setValueFromMmsDataObj(Data data) throws ServiceError {
    if (data.getUtcTime() == null) {
      throw new ServiceError(ServiceError.TYPE_CONFLICT, "expected type: utc_time/timestamp");
    }
    value = data.getUtcTime().value;
  }

  @Override
  TypeDescription getMmsTypeSpec() {
    TypeDescription typeDescription = new TypeDescription();
    typeDescription.setUtcTime(new BerNull());
    return typeDescription;
  }

  @Override
  public String toString() {
    return getReference().toString() + ": " + getDate();
  }

  @Override
  public String getValueString() {
    return getDate().toString();
  }
}
